/*
 * Copyright (C) 2000-2008 Jens Nixdorf and various contributors
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to
 * deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies of the Software, its documentation and marketing & publicity
 * materials, and acknowledgment shall be given in the documentation, materials
 * and software packages that this Software was used.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package org.mopore.jat.plugin;

import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import javax.swing.JOptionPane;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.mopore.jat.board.BoardFeaturePlugin;
import org.mopore.jat.core.CorePlatformPlugin;
import org.mopore.jat.core.ICore;
import org.mopore.jat.core.externalplugins.ExternalPluginManager;
import org.mopore.jat.core.messaging.Message;
import org.mopore.jat.core.messaging.MessageType;
import org.mopore.jat.gui.SysTrayProxy;
import org.mopore.jat.gui.Theme;
import org.mopore.jat.gui.WindowController;
import org.mopore.jat.plugin.IJatPlugin.PluginLoadState;
import org.mopore.jat.timer.TimerPlugin;


public class PluginEngine {
	
	private static final String PLUGIN_CAN_NOT_HANDLE_GAINING_FOUCUS = "plugin can not handle gaining foucus: ";

	private static final String ERROR_LOADING_PLUGIN = " Error loading plugin";
	
	private static final String ERROR_STARTING_PLUGIN = " Error starting plugin";
	
	private static final String ERROR_SHUTTING_DOWN_PLUGIN = " Error shutting down plugin: ";
	
	private static final String ERROR_LOADING_META_INFORMATION = "Error loading meta information for: ";
	
	private final Log log = LogFactory.getLog(getClass().getName());
	
	private ExecutorService executerService;
	
	private Map<String, Future<Class<Void>>> futureMap;
	
	private final IPluginContextInternal pluginContext;
	
	private String currentFocusOn;
	
	private final SysTrayProxy sysTrayProxy;
	
	private final ThemeChangeDelegator themeChangeDelegator;
	
	private Map<String, String> blackList;

	private ICore core;
	
	private final ExternalPluginManager externalPluginManager;

	private final WindowController windowController;

	
	public PluginEngine( SysTrayProxy sysTrayProxy ) {
		
		this.sysTrayProxy = sysTrayProxy;
		this.themeChangeDelegator = new ThemeChangeDelegator();
		this.windowController = new WindowController();
		
		currentFocusOn = null;
		executerService = null;
		
		blackList = new HashMap<String, String>();
		this.futureMap = new LinkedHashMap<String, Future<Class<Void>>>();
		
		try {
			externalPluginManager = new ExternalPluginManager( this );
			Map<String, IJatPluginInternal> plugins;
			plugins = collectPlugins();
			pluginContext = new PluginContextBaseImpl( this, plugins, sysTrayProxy );
			themeChangeDelegator.addListener( pluginContext );
			loadPlugins();
			removePluginsNotLoaded();
		} catch (PluginException e) {
			String message = "Fatal error can not initialize plugin engine!";
			log.error( message, e );
			JOptionPane.showMessageDialog( null, message, "fatal error", 
					JOptionPane.ERROR_MESSAGE );
			throw new InternalError( message );
		}
	}


	public void runPlugins() {
		
		Map<String, IJatPluginInternal> pluginMap = pluginContext.getPlugInMap();
		
		int size = pluginMap.size();
		log.debug( "Creating executer service for " + size + " plugins..." );
		executerService = Executors.newCachedThreadPool();
		
		Set<String>	 keySet = pluginMap.keySet();
		log.info( "Running Plugins now..." );
		
		for (String key : keySet) {
			runPlugin(pluginMap, key);
		}
		
		String nameToFocus = core.getFocus();
		
		// set the focus 
		if ( nameToFocus != null ) {
			IJatPlugin plugin = pluginMap.get( nameToFocus );
			if ( plugin == null ) {
				nameToFocus = null;
			}
			else {
				nameToFocus = plugin.getClass().getName();
				PluginLoadState state = plugin.getLoadState();
				boolean notRunning = ( state != PluginLoadState.RUNNING );
				
				if ( notRunning ) {
					nameToFocus = null;
				}
			}
		}

		// try to find only candidate
		if ( nameToFocus == null ) {
			List<IJatPluginInternal> list = pluginContext.getFocusablePlugins();
			if ( list.size() == 1 ) {
				IJatPluginInternal candidate = list.get( 0 );
				PluginLoadState state = candidate.getLoadState();
				boolean notRunning = ( state != PluginLoadState.RUNNING );
				
				if ( notRunning ) {
					nameToFocus = null;
				}
				else {
					try {
						nameToFocus = candidate.getPluginName();
					} catch (PluginException e) {
						log.error( e );
					}
				}
			}
		}
		
		changeFocus( nameToFocus );
	}


	private void runPlugin(Map<String, IJatPluginInternal> pluginMap, String pluginName) {
		log.info( "Starting: " + pluginName );
		IJatPluginInternal plugin = pluginMap.get( pluginName );
		
		log.debug( "now submitting callable plugin to execeuterservice.. " );
		
		Future<Class<Void>> future;
		try {
			future = executerService.submit( plugin );
			log.info( "plugin started: " + pluginName );
			futureMap.put( pluginName,  future );
			pluginContext.setPluginLoadState(pluginName, PluginLoadState.RUNNING );
		} catch (Exception e) {
			log.error( "plugin could not be started: " + pluginName, e );
			
			terminatePlugin( pluginName, ERROR_STARTING_PLUGIN );
		}
	}


	private void removePluginsNotLoaded() {
		Map<String, IJatPluginInternal> map = pluginContext.getPlugInMap();
		
		Set<String> keySet = new HashSet<String>( map.keySet() );
		
		for (String key : keySet) {
			
			PluginLoadState loadState = pluginContext.getPluginLoadState( key );
			if ( PluginLoadState.LOADED == loadState ) {
				log.info( "plugin loaded: " + key );
			}
			else {
				log.error( "removing plugin: " + key );
				map.remove( key );
			}
		}
		
	}


	private void loadPlugins() {
		
		Map<String, IJatPluginInternal> pluginMap = 
			pluginContext.getPlugInMap();
		
		Set<String> keySet = pluginMap.keySet();
		for (String pluginName : keySet) {
			
			loadPlugin(pluginMap, pluginName);
		}
	}


	private void loadPlugin(Map<String, IJatPluginInternal> pluginMap,
			String pluginName) {
		IJatPluginInternal plugin = null;
		try {
			log.info( "try to load plugin: " + pluginName );
			plugin = pluginMap.get( pluginName );
			IPluginContext pluginContextWrapper = 
				new PluginContextWrapperAgent( pluginContext, plugin );
			plugin.onPluginLoad( pluginContextWrapper );
			
			pluginContext.setPluginLoadState( pluginName, PluginLoadState.LOADED );
		} catch (PluginException e) {
			log.error( ERROR_LOADING_PLUGIN, e );
			
			terminatePlugin( pluginName, ERROR_LOADING_PLUGIN);
		}
	}


	private Map<String, IJatPluginInternal> collectPlugins() throws PluginException {
		
		Map<String, IJatPluginInternal> plugins = new LinkedHashMap<String, IJatPluginInternal>();
		
		log.info( "Collecting internal plugins..." );
		CorePlatformPlugin corePlatformPlugin = 
			new CorePlatformPlugin( sysTrayProxy, themeChangeDelegator, 
					externalPluginManager, windowController );
		this.core = corePlatformPlugin;
		TimerPlugin timerPlugin = new TimerPlugin();
		BoardFeaturePlugin boardFeaturePlugin = new BoardFeaturePlugin();
		plugins.put( CorePlatformPlugin.class.getName(), corePlatformPlugin );
//		plugins.put( TimerPlugin.class.getName(), timerPlugin );
//		plugins.put( BoardFeaturePlugin.class.getName(), boardFeaturePlugin );
		log.info( "internal plugins collected." );
		
		log.info( "Collecting external plugins..." );
		Map<String, IJatPluginInternal> externalPlugins = 
			externalPluginManager.collectExternalPlugins();
		
		Set<String> keys = externalPlugins.keySet();
		for (String key : keys ) {
			IJatPluginInternal plugin = externalPlugins.get( key );
			plugins.put( key, plugin );
		}
		log.info( "external plugins colleted..." );
		
		return plugins;
	}


	public boolean shutDownPlugin( String pluginName ) {
		
		Boolean clearShutDown = null;
		
		log.info( "shutting down plugin: " + pluginName );
		
		log.info( "calling shut down hook" );
		Map<String, IJatPluginInternal> pluginMap = 
			pluginContext.getPlugInMap();
		IJatPluginInternal plugin = pluginMap.get( pluginName );
		try {
			plugin.onPluginShutDown();
		} catch (PluginException e) {
			log.error( "An exception was thrown when shutdown hook was called", e );
			clearShutDown = false;
			// shutdown will performed anyway...
		}
		
		Future<Class<Void>> future = futureMap.get( pluginName );
		
		try {
			log.info( "Will wait for thread ending (max: 1 second)" );
			try {
				future.get( 1, TimeUnit.SECONDS );
				if ( clearShutDown == null ) {
					clearShutDown = true;
				}
			} catch (TimeoutException e) {
				
				log.error( "Time out extended. Thread will be canceled...", e );
				future.cancel( true );
			}
			
		} catch (Exception e) {
			log.error( "error: ", e );
			if ( ! future.isCancelled() ) {
				try {
					future.cancel( true );
				}
				catch (Exception e1) {
					// we don't care anymore
				}
			}
		}
		futureMap.remove( pluginName );
		pluginContext.setPluginLoadState( pluginName, PluginLoadState.UNLOADED );
		
		if ( clearShutDown == null ) {
			clearShutDown = false;
		}
		
		pluginMap.remove( pluginName );
		
		return clearShutDown;
	}
	
	
	public void changeFocus( String pluginName ) {
		
		
		boolean noFocusChanged = false;
		
		if ( currentFocusOn != null ) {
			noFocusChanged = currentFocusOn.equals( pluginName );
		}
		else {
			noFocusChanged = ( pluginName == null );
		}
		
		if( noFocusChanged ) {
			return;
		}
		
		boolean focusIsset = ( currentFocusOn != null );
		IJatPluginInternal oldplugin = null;
		if ( focusIsset ) {
			oldplugin = takeFocusAway();
		}
		setFocusNew(pluginName, oldplugin);
	}


	private IJatPluginInternal takeFocusAway() throws InternalError {
		
		IJatPluginInternal oldplugin = 
			pluginContext.getPlugInMap().get( currentFocusOn );
		
		try {
			pluginContext.onFocusLost( oldplugin );
		} catch (PluginException e) {
			String message = "plugin can not handle loss of foucus: " + 
				currentFocusOn ;
			
			log.error( message );
			terminatePlugin( currentFocusOn, message );
		}
		return oldplugin;
	}
	
	
	private void setFocusNew(String pluginName, IJatPluginInternal oldplugin) throws InternalError {
		
		IJatPluginInternal plugin = 
			pluginContext.getPlugInMap().get( pluginName );
		
		try {
			if ( plugin != null ) {
				pluginContext.onFocusGained( plugin );
			}
			currentFocusOn = pluginName;
			core.storeFocus( currentFocusOn );
			log.info( "focus successfully changed to: " + currentFocusOn );
		} catch (PluginException e) {
			log.error( PLUGIN_CAN_NOT_HANDLE_GAINING_FOUCUS + pluginName );
			if ( oldplugin == null ) {
				return;
			}
			putFocusOnOldPlugin( oldplugin );
			if ( currentFocusOn != null ) {
				terminatePlugin( currentFocusOn, PLUGIN_CAN_NOT_HANDLE_GAINING_FOUCUS + pluginName );
			}
		}
	}


	private void putFocusOnOldPlugin(IJatPluginInternal oldplugin ) {
		log.error( "old plugin gets focus back" );
		try {
			pluginContext.onFocusGained( oldplugin );
		} catch (PluginException e) {
			log.error( PLUGIN_CAN_NOT_HANDLE_GAINING_FOUCUS + oldplugin , e );
			terminatePlugin( currentFocusOn, PLUGIN_CAN_NOT_HANDLE_GAINING_FOUCUS + oldplugin );
		}
	}


	
	public void shutdownEngine() {
		
		Map<String, IJatPluginInternal> pluginMap = pluginContext.getPlugInMap();
		
		Set<String>	 keySet = new HashSet<String>();
		keySet.addAll( pluginMap.keySet() ); 

		log.info( "shutting down plugins now..." );
		
		for (String key : keySet) {
			
			log.info( "Shutting down: " + key );
			IJatPluginInternal plugin = pluginMap.get( key );
			if ( plugin == null ) {
				// a plugin might be already shut down...
				continue;
			}
			
			IPluginMetaInformation metaInfo = null;
			
			try {
				metaInfo = plugin.getMetaInformation();
			} 
			catch (PluginException e) {
				String title = ERROR_SHUTTING_DOWN_PLUGIN;
				String text = ERROR_LOADING_META_INFORMATION + key;
				Message errorMessage = new Message( title, text, MessageType.ERROR );
				sysTrayProxy.showMessage( errorMessage );
				
				log.error( ERROR_LOADING_META_INFORMATION + key, e );
				
				blackList.put( key, ERROR_LOADING_META_INFORMATION + key );
			}
			
			boolean clearShutDown = shutDownPlugin( key );
			
			if ( clearShutDown ) {
				log.info( "plugin shutdown succeeded for: " + key ); 
			}
			else {
				String info = metaInfo.toString();
				String text = ERROR_SHUTTING_DOWN_PLUGIN + key;
				
				log.error( text );
				log.info( info );
				
				Message errorMessage = new Message( text, info, MessageType.ERROR );
				sysTrayProxy.showMessage( errorMessage );
				
				
				log.error( ERROR_SHUTTING_DOWN_PLUGIN );
				
				blackList.put( key, text );
			}
		}
		log.info( "Shutting down engine..." );
		executerService.shutdownNow();
		log.info( "Engine is down." );
		System.exit( 0 );
	}
	
	
	public IPluginContextInternal getPluginContextInternal() {
		return pluginContext;
	}

	
	public Map<String, String> getBlackList(){
		return blackList;
	}



	public String getPluginWithFocus() {
		return currentFocusOn;
	}
	
	
	public void terminatePlugin( String pluginName, String errorTitle ) {
	
		log.error( "Terminating: " + pluginName );
		
		Map<String, IJatPluginInternal> pluginMap =  
			pluginContext.getPlugInMap();
		
		IPluginMetaInformation metaInfo = null;
		try {
			IJatPlugin plugin = pluginMap.get( pluginName );
			metaInfo = plugin.getMetaInformation();
		}
		catch( Exception e) {
			log.error( ERROR_LOADING_META_INFORMATION + pluginName, e );
			// we be handled by evaluating metInfo...
		}
		
		pluginMap.remove( pluginName );
		pluginContext.onTerminated( pluginName );
		
		String infoMessage;
		if ( metaInfo != null ) {
			infoMessage = "unloaded plugin:\n\n" + metaInfo.toString();
		}
		else {
			infoMessage = ERROR_LOADING_META_INFORMATION + pluginName;
		}
	
		Message errorMessage = new Message( errorTitle, infoMessage, org.mopore.jat.core.messaging.MessageType.ERROR );
		sysTrayProxy.showMessage( errorMessage );
		log.error( errorTitle );
		log.error( infoMessage );
		
		blackList.put( pluginName, errorTitle );
		
		Future<Class<Void>> future = futureMap.get( pluginName );
		
		try {
			log.error( "Plugin thread will be canceled..." );
			future.cancel( true );
			
		} catch (Exception e) {
			if ( ! future.isCancelled() ) {
				try {
					future.cancel( true );
				}
				catch (Exception e1) {
					// we don't care anymore
				}
			}
		}
		futureMap.remove( pluginName );
		log.error( "Plugin thread is removed." );
		changeFocus( null );
		
	}


	public Theme getCurrentTheme() {
		return core.getTheme();
	}


	public ExternalPluginManager getExternalPluginManager() {
		return externalPluginManager;
	}


	public void informPluginsChange() {
		sysTrayProxy.imformPluginsChange();
	}


	public void postLoadPlugin(IJatPluginInternal plugin) throws PluginException {
		Map<String, IJatPluginInternal> pluginMap = 
			pluginContext.getPlugInMap();
		String pluginName = plugin.getPluginName();
		pluginContext.setPluginLoadState( pluginName, PluginLoadState.NOTLOADED );
		pluginMap.put( pluginName , plugin);
		loadPlugin( pluginMap, pluginName );
		runPlugin( pluginMap, pluginName );
		
		PluginLoadState state = plugin.getLoadState();
		boolean notRunning = ( state != PluginLoadState.RUNNING );
		
		if ( notRunning ) {
			String message = "plugin " + pluginName + " could not be started.";
			log.error( message );
			throw new PluginException( message );
		}
		
		boolean focusNotSet = ( currentFocusOn == null );
		if ( focusNotSet ) {
			if ( plugin.isFocusable() ) {
				changeFocus( pluginName );
			}
		}
		informPluginsChange();
	}


	public WindowController getWindowController() {
		return windowController;
	}	
}
